package com.nulldev.util.web.YouTubeExtractor.subtitles.types;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedTransferQueue;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Returnable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.YouTubeExtractor.subtitles.Subtitle;
import com.nulldev.util.web.YouTubeExtractor.subtitles.SubtitleParser;
import com.nulldev.util.web.YouTubeExtractor.subtitles.Subtitle.SubtitleStyle;
import com.nulldev.util.web.YouTubeExtractor.subtitles.SubtitleParser.FMTVersion;

public class JSONSubtitleParser {
	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--Subtitles.debug"));

	private static final int MT_SUBTITLE_GEN_THRESHOLD = 60000;

	private static Returnable<Subtitle> __subtitle_process(final int pos, final boolean allowStyling, final UniJsonIterableValue e, final List<Subtitle> out,
			final UniJsonIterableValue p, final UniJsonIterableValue pens) {
		return new Returnable<Subtitle>() {

			@Override
			public Subtitle run() {
				final UniJsonValue o = e.get(pos);
				final int positionId = o.getInt("wpWinPosId", pos);
				final UniJsonValue f = p.getIfInList(positionId);
				try {
					final StringBuilder text = new StringBuilder();
					final UniJsonValue segs = o.get("segs");
					final List<SubtitleStyle> styles = FastLists.list(16);
					if (segs != null && !segs.isNull())
						segs.asArray().forEach((c) -> {
							final boolean hasPenId = c.contains("pPenId") || o.contains("pPenId");
							if (hasPenId && allowStyling && SubtitleParser.ENABLE_PEN_FORMATTING) {
								final int index = c.contains("pPenId") ? c.getInt("pPenId", -1) : o.getInt("pPenId", -1);
								final UniJsonValue pen = index <= -1 ? null : pens.getIfInList(index);
								final StringBuilder sb = new StringBuilder(c.getString("utf8"));
								if (pen != null && !pen.isNull()) {
									final SubtitleStyle lPen = new SubtitleStyle();
									pen.map().forEach((key, item) -> {
										if (key.equals("fcForeColor")) {
											lPen.color = pen.getInt("fcForeColor", -1);
											sb.insert(0, "<c." + lPen.textIdf + ">");
											sb.append("</c>");
										} else if (key.equals("boBackAlpha")) {
											final int alpha = pen.getInt("boBackAlpha");
											if (alpha >= 254 || alpha == 0) {
												lPen.backgroundAlpha = MathUtil.clamp(0, 254, alpha);
											} else
												log.warn("Unsupported background alpha: " + alpha);
										} else if (key.equals("bcBackColor")) {
											final int color = pen.getInt("bcBackColor");
											if (color == 0)
												lPen.backgroundAlpha = 0;
											else
												log.warn("Unsupported property: 'backgroundColor' (value: " + color + ")");
										} else if (key.equals("foForeAlpha")) {
											final int alpha = pen.getInt("foForeAlpha");
											lPen.foregroundAlpha = MathUtil.clamp(0, 254, alpha);
										} else if (key.equals("szPenSize")) {
											final float size = pen.getInt("szPenSize");
											if (size != 100)
												lPen.textSizeIncrement = (float) (1 + .25 * (size / (100 - 1)));
										} else if (key.equals("ecEdgeColor")) {
											final int edgeColor = pen.getInt("ecEdgeColor");
											lPen.edgeColor = edgeColor;
										} else if (key.equals("fsFontStyle")) {
											lPen.fontFamily = pen.getInt("fsFontStyle");
										} else if (key.equals("iAttr") && pen.getInt("iAttr", 0) == 1) {
											sb.insert(0, "<i>");
											sb.append("</i>");
										} else if (key.equals("bAttr") && pen.getInt("bAttr", 0) == 1) {
											sb.insert(0, "<b>");
											sb.append("</b>");
										} else if (key.equals("uAttr") && pen.getInt("uAttr", 0) == 1) {
											sb.insert(0, "<u>");
											sb.append("</u>");
										} else if (key.equals("etEdgeType")) { // Not implemented since these mess with text-shadow dimensions
											log.debug("Dropping etEdgeType with value " + item + ".");
										} else
											log.warn("Unknown pen property: " + key + " (value: " + item + ")");
									});
									styles.add(lPen);
									text.append(sb.toString());
								} else {
									text.append(sb.toString());
								}
							} else
								text.append(c.getString("utf8"));
						});
					final int start = o.getInt("tStartMs");
					final int durRaw = !o.hasValue("dDurationMs") ? 200 : (o.getInt("dDurationMs"));
					if (!o.hasValue("dDurationMs"))
						log.dwarn("dDurationMs is missing for: " + o);
					log.debug("Subtitle: \"" + text.toString() + "\", start: " + start + ", dur_raw: " + durRaw);
					final int h = (f != null ? f.getInt("ahHorPos", -1) : -1);
					final int v = (f != null ? f.getInt("avVerPos", -1) : -1);
//					if (f != null) f.println();
//					log.info("Subtitle.Position: vertical: " + v + ", horizontal: " + h);
					return new Subtitle(SubtitleParser.ENABLE_BR_HACK ? StringsUtil.replace(text.toString(), "\n", "<br>") : text.toString(), start, durRaw,
							styles, true).setPositionH(h).setPositionV(v);
				} catch (Exception ex) {
					log.error("Failed to process subtitle entry. [o=" + o + "]", ex);
					return null;
				}
			}
		};
	}

	public static List<Subtitle> getSubtitlesJSON(final String data, final FMTVersion ver) {
		return getSubtitlesJSON(data, true, ver);
	}

	public static List<Subtitle> getSubtitlesJSON(final String data, final boolean allowStyling, final FMTVersion ver) {
		if (data == null)
			throw new NullPointerException("getSubtitles() -> data == null!");
		if (data.startsWith("<"))
			throw new NullPointerException("getSubtitles() -> Invalid data! (HTML payload?)");
		final List<Subtitle> out = FastLists.list(32);
		final UniJsonValue u = UniJsonValue.parse(data);
		if (!u.hasValue("events"))
			throw new NullPointerException("getSubtitles() -> Could not find: events");
		final UniJsonIterableValue p = u.get("wpWinPositions").asArray();
		final UniJsonIterableValue pens = u.get("pens").asArray();
		final UniJsonIterableValue e = u.get("events").asArray();
		if (e.length() > MT_SUBTITLE_GEN_THRESHOLD) { // Only use MT if we are a very big subtitle file
			final LinkedTransferQueue<Executable<Object>> tasks = new LinkedTransferQueue<Executable<Object>>();
			final Map<Integer, Subtitle> subtitles = new ConcurrentHashMap<Integer, Subtitle>(e.length());
			for (int i = 0; i < e.length(); i++) {
				final int pos = i;
				try {
					// FIXME: Shouldn't we be using the APPEND_ACCELERATOR from WebVTTBuilder? (or
					// at least make it local to the package)
					tasks.put(ExecutorManager.globalAsyncSubmit(() -> {
						subtitles.put(pos, __subtitle_process(pos, allowStyling, e, out, p, pens).run());
						return null;
					}));
				} catch (Exception e1) {
					throw new RuntimeException(e1);
				}
			}
			ExecutorManager.get().awaitQueue(tasks);
			for (int i = 0; i < e.length(); i++) {
				out.add(subtitles.get(i));
			}
			/* return Lists.of((Subtitle[]) subtitles.values().toArray()); */
		} else {
			for (int i = 0; i < e.length(); i++) {
				try {
					out.add(__subtitle_process(i, allowStyling, e, out, p, pens).run());
				} catch (Exception ex) {
					log.error("Failed to generate subtitle!", ex);
				}
			}
		}
		return out;
	}
}
